dash_charts.time_vis_chart⚓︎
R daattali/TimeVis-like charts for displaying chronological data.
See daattali/TimeVis: https://github.com/daattali/timevis
NOTE: Consider automated (non-overlapping) text/event placement⚓︎
- MATLAB Adjust Text: https://github.com/Phlya/adjustText
- D3 Labeler: https://github.com/tinker10/D3-Labeler
- Plotly has implementation for contour? https://github.com/plotly/plotly.js/issues/4674#issuecomment-603571483
View Source
"""R daattali/TimeVis-like charts for displaying chronological data.
See daattali/TimeVis: https://github.com/daattali/timevis
# NOTE: Consider automated (non-overlapping) text/event placement
- MATLAB Adjust Text: https://github.com/Phlya/adjustText
- D3 Labeler: https://github.com/tinker10/D3-Labeler
- Plotly has implementation for contour? https://github.com/plotly/plotly.js/issues/4674#issuecomment-603571483
"""
import numpy as np
import plotly.graph_objects as go
from .utils_data import DASHED_TIME_FORMAT_YEAR, GDP_TIME_FORMAT, format_unix, get_unix
from .utils_fig import CustomChart
class TimeVisChart(CustomChart): # noqa: H601
"""Time Vis Chart: resource use timeline."""
date_format = DASHED_TIME_FORMAT_YEAR
"""Date format for bar chart. Default is `DASHED_TIME_FORMAT_YEAR`."""
fillcolor = '#D5DDF6'
"""Default fillcolor for time vis events."""
hover_label_settings = {'bgcolor': 'white', 'font_size': 12, 'namelength': 0}
"""Plotly hover label settings."""
rh = 1
"""Height of each rectangular time vis."""
y_space = -1.5 * rh
"""Vertical spacing between rectangles."""
categories = None
"""List of string category names set in self.create_traces()."""
_shapes = []
"""List of shapes for plotly layout."""
def create_traces(self, df_raw): # noqa: CCR001
"""Return traces for plotly chart.
Args:
df_raw: pandas dataframe with columns: `(category, label, start, end)`
Returns:
list: Dash chart traces
"""
# Get all unique category names and create lookup for y positions
self.categories = sorted(cat for cat in set(df_raw['category'].tolist()) if cat)
y_pos_lookup = {cat: self.y_space * idx for idx, cat in enumerate(self.categories)}
# Create the Time Vis traces
traces = []
self._shapes = []
self._annotations = []
for vis in df_raw.itertuples():
if vis.category in y_pos_lookup:
y_pos = y_pos_lookup[vis.category]
if vis.end:
traces.append(self._create_time_vis_shape(vis, y_pos))
if vis.label:
traces.append(self._create_annotation(vis, y_pos))
else:
traces.append(self._create_event(vis, y_pos))
else:
y_pos = 0
traces.append(self._create_non_cat_shape(vis, y_pos))
return traces
def _create_hover_text(self, vis):
"""Return hover text for given trace.
Args:
vis: row tuple from df_raw with: `(category, label, start, end)`
Returns:
string: HTML-formatted hover text
"""
new_format = f'%a, {GDP_TIME_FORMAT}'
start_date = format_unix(get_unix(vis.start, self.date_format), new_format)
if vis.end:
end_date = format_unix(get_unix(vis.end, self.date_format), new_format)
date_range = f'<b>Start</b>: {start_date}<br><b>End</b>: {end_date}'
else:
date_range = f'<b>Event</b>: {start_date}'
return f'<b>{vis.category}</b><br>{vis.label}<br>{date_range}'
def _create_non_cat_shape(self, vis, y_pos):
"""Create non-category time visualization (vertical across all categories).
Note: background shape is set below a transparent trace so that hover works
Args:
vis: row tuple from df_raw with: `(category, label, start, end)`
y_pos: top y-coordinate of vis
Returns:
trace: single Dash chart Scatter trace
"""
bot_y = self.y_space * len(self.categories)
self._shapes.append(
go.layout.Shape(
fillcolor=self.fillcolor,
layer='below',
line={'width': 0},
opacity=0.4,
type='rect',
x0=vis.start,
x1=vis.end,
xref='x',
y0=bot_y,
y1=y_pos,
yref='y',
),
)
return go.Scatter(
fill='toself',
opacity=0,
hoverlabel=self.hover_label_settings,
line={'width': 0},
mode='lines',
text=self._create_hover_text(vis),
x=[vis.start, vis.end, vis.end, vis.start, vis.start],
y=[y_pos, y_pos, bot_y, bot_y, y_pos],
)
def _create_time_vis_shape(self, vis, y_pos):
"""Create filled rectangle for time visualization.
Args:
vis: row tuple from df_raw with: `(category, label, start, end)`
y_pos: top y-coordinate of vis
Returns:
trace: single Dash chart Scatter trace
"""
return go.Scatter(
fill='toself',
fillcolor=self.fillcolor,
hoverlabel=self.hover_label_settings,
line={'width': 0},
mode='lines',
text=self._create_hover_text(vis),
x=[vis.start, vis.end, vis.end, vis.start, vis.start],
y=[y_pos, y_pos, y_pos - self.rh, y_pos - self.rh, y_pos],
)
def _create_annotation(self, vis, y_pos):
"""Add vis label to chart as text overlay.
Args:
vis: row tuple from df_raw with: `(category, label, start, end)`
y_pos: top y-coordinate of vis
Returns:
trace: single Dash chart Scatter trace
"""
return go.Scatter(
hoverlabel=self.hover_label_settings,
hovertemplate=self._create_hover_text(vis) + '<extra></extra>',
hovertext=self._create_hover_text(vis),
mode='text',
text=vis.label,
textposition='middle right',
x=[vis.start],
y=[y_pos - self.rh / 2],
)
def _create_event(self, vis, y_pos):
"""Create singular event with vertical line, marker, and text.
If label is longer than 10 characters, then the annotation is shown offset with an arrow.
Args:
vis: row tuple from df_raw with: `(category, label, start, end)`
y_pos: top y-coordinate of vis
Returns:
trace: single Dash chart Scatter trace
"""
if len(vis.label) > 10:
self._annotations.append({
'align': 'right',
'arrowcolor': self.fillcolor,
'showarrow': True,
'arrowhead': 2,
'text': vis.label,
'x': vis.start,
'xanchor': 'right',
'y': y_pos - self.rh / 2,
'yanchor': 'middle',
})
self._shapes.append(
go.layout.Shape(
layer='below',
line={
'color': self.fillcolor,
'dash': 'longdashdot',
'width': 2,
},
type='line',
x0=vis.start,
x1=vis.start,
xref='x',
y0=self.y_space * len(self.categories),
y1=y_pos - self.rh / 2,
yref='y',
),
)
return go.Scatter(
hoverlabel=self.hover_label_settings,
hovertemplate=self._create_hover_text(vis) + '<extra></extra>',
hovertext=self._create_hover_text(vis),
marker={'color': self.fillcolor},
mode='markers+text',
text='' if len(vis.label) > 10 else vis.label,
textposition='top center',
x=[vis.start],
y=[y_pos - self.rh / 2],
)
def create_layout(self):
"""Extend the standard layout.
Returns:
dict: layout for Dash figure
"""
layout = super().create_layout()
# Set YAxis tick marks for category names (https://plotly.com/python/tick-formatting)
layout['yaxis']['tickmode'] = 'array'
layout['yaxis']['tickvals'] = np.subtract(
np.multiply(
np.array(range(len(self.categories))),
self.y_space,
),
self.rh / 2,
)
layout['yaxis']['ticktext'] = [*self.categories]
layout['yaxis']['zeroline'] = False
# Hide legend
layout['legend'] = {}
layout['showlegend'] = False
# Add shapes and append new annotations
layout['shapes'] = self._shapes
layout['annotations'] += self._annotations
return layout
Variables⚓︎
DASHED_TIME_FORMAT_YEAR
GDP_TIME_FORMAT
Classes⚓︎
TimeVisChart⚓︎
class TimeVisChart(
*,
title,
xlabel,
ylabel,
layout_overrides=()
)
View Source
class TimeVisChart(CustomChart): # noqa: H601
"""Time Vis Chart: resource use timeline."""
date_format = DASHED_TIME_FORMAT_YEAR
"""Date format for bar chart. Default is `DASHED_TIME_FORMAT_YEAR`."""
fillcolor = '#D5DDF6'
"""Default fillcolor for time vis events."""
hover_label_settings = {'bgcolor': 'white', 'font_size': 12, 'namelength': 0}
"""Plotly hover label settings."""
rh = 1
"""Height of each rectangular time vis."""
y_space = -1.5 * rh
"""Vertical spacing between rectangles."""
categories = None
"""List of string category names set in self.create_traces()."""
_shapes = []
"""List of shapes for plotly layout."""
def create_traces(self, df_raw): # noqa: CCR001
"""Return traces for plotly chart.
Args:
df_raw: pandas dataframe with columns: `(category, label, start, end)`
Returns:
list: Dash chart traces
"""
# Get all unique category names and create lookup for y positions
self.categories = sorted(cat for cat in set(df_raw['category'].tolist()) if cat)
y_pos_lookup = {cat: self.y_space * idx for idx, cat in enumerate(self.categories)}
# Create the Time Vis traces
traces = []
self._shapes = []
self._annotations = []
for vis in df_raw.itertuples():
if vis.category in y_pos_lookup:
y_pos = y_pos_lookup[vis.category]
if vis.end:
traces.append(self._create_time_vis_shape(vis, y_pos))
if vis.label:
traces.append(self._create_annotation(vis, y_pos))
else:
traces.append(self._create_event(vis, y_pos))
else:
y_pos = 0
traces.append(self._create_non_cat_shape(vis, y_pos))
return traces
def _create_hover_text(self, vis):
"""Return hover text for given trace.
Args:
vis: row tuple from df_raw with: `(category, label, start, end)`
Returns:
string: HTML-formatted hover text
"""
new_format = f'%a, {GDP_TIME_FORMAT}'
start_date = format_unix(get_unix(vis.start, self.date_format), new_format)
if vis.end:
end_date = format_unix(get_unix(vis.end, self.date_format), new_format)
date_range = f'<b>Start</b>: {start_date}<br><b>End</b>: {end_date}'
else:
date_range = f'<b>Event</b>: {start_date}'
return f'<b>{vis.category}</b><br>{vis.label}<br>{date_range}'
def _create_non_cat_shape(self, vis, y_pos):
"""Create non-category time visualization (vertical across all categories).
Note: background shape is set below a transparent trace so that hover works
Args:
vis: row tuple from df_raw with: `(category, label, start, end)`
y_pos: top y-coordinate of vis
Returns:
trace: single Dash chart Scatter trace
"""
bot_y = self.y_space * len(self.categories)
self._shapes.append(
go.layout.Shape(
fillcolor=self.fillcolor,
layer='below',
line={'width': 0},
opacity=0.4,
type='rect',
x0=vis.start,
x1=vis.end,
xref='x',
y0=bot_y,
y1=y_pos,
yref='y',
),
)
return go.Scatter(
fill='toself',
opacity=0,
hoverlabel=self.hover_label_settings,
line={'width': 0},
mode='lines',
text=self._create_hover_text(vis),
x=[vis.start, vis.end, vis.end, vis.start, vis.start],
y=[y_pos, y_pos, bot_y, bot_y, y_pos],
)
def _create_time_vis_shape(self, vis, y_pos):
"""Create filled rectangle for time visualization.
Args:
vis: row tuple from df_raw with: `(category, label, start, end)`
y_pos: top y-coordinate of vis
Returns:
trace: single Dash chart Scatter trace
"""
return go.Scatter(
fill='toself',
fillcolor=self.fillcolor,
hoverlabel=self.hover_label_settings,
line={'width': 0},
mode='lines',
text=self._create_hover_text(vis),
x=[vis.start, vis.end, vis.end, vis.start, vis.start],
y=[y_pos, y_pos, y_pos - self.rh, y_pos - self.rh, y_pos],
)
def _create_annotation(self, vis, y_pos):
"""Add vis label to chart as text overlay.
Args:
vis: row tuple from df_raw with: `(category, label, start, end)`
y_pos: top y-coordinate of vis
Returns:
trace: single Dash chart Scatter trace
"""
return go.Scatter(
hoverlabel=self.hover_label_settings,
hovertemplate=self._create_hover_text(vis) + '<extra></extra>',
hovertext=self._create_hover_text(vis),
mode='text',
text=vis.label,
textposition='middle right',
x=[vis.start],
y=[y_pos - self.rh / 2],
)
def _create_event(self, vis, y_pos):
"""Create singular event with vertical line, marker, and text.
If label is longer than 10 characters, then the annotation is shown offset with an arrow.
Args:
vis: row tuple from df_raw with: `(category, label, start, end)`
y_pos: top y-coordinate of vis
Returns:
trace: single Dash chart Scatter trace
"""
if len(vis.label) > 10:
self._annotations.append({
'align': 'right',
'arrowcolor': self.fillcolor,
'showarrow': True,
'arrowhead': 2,
'text': vis.label,
'x': vis.start,
'xanchor': 'right',
'y': y_pos - self.rh / 2,
'yanchor': 'middle',
})
self._shapes.append(
go.layout.Shape(
layer='below',
line={
'color': self.fillcolor,
'dash': 'longdashdot',
'width': 2,
},
type='line',
x0=vis.start,
x1=vis.start,
xref='x',
y0=self.y_space * len(self.categories),
y1=y_pos - self.rh / 2,
yref='y',
),
)
return go.Scatter(
hoverlabel=self.hover_label_settings,
hovertemplate=self._create_hover_text(vis) + '<extra></extra>',
hovertext=self._create_hover_text(vis),
marker={'color': self.fillcolor},
mode='markers+text',
text='' if len(vis.label) > 10 else vis.label,
textposition='top center',
x=[vis.start],
y=[y_pos - self.rh / 2],
)
def create_layout(self):
"""Extend the standard layout.
Returns:
dict: layout for Dash figure
"""
layout = super().create_layout()
# Set YAxis tick marks for category names (https://plotly.com/python/tick-formatting)
layout['yaxis']['tickmode'] = 'array'
layout['yaxis']['tickvals'] = np.subtract(
np.multiply(
np.array(range(len(self.categories))),
self.y_space,
),
self.rh / 2,
)
layout['yaxis']['ticktext'] = [*self.categories]
layout['yaxis']['zeroline'] = False
# Hide legend
layout['legend'] = {}
layout['showlegend'] = False
# Add shapes and append new annotations
layout['shapes'] = self._shapes
layout['annotations'] += self._annotations
return layout
Ancestors (in MRO)⚓︎
- dash_charts.utils_fig.CustomChart
Class variables⚓︎
annotations
categories
List of string category names set in self.create_traces().
date_format
Date format for bar chart. Default is DASHED_TIME_FORMAT_YEAR.
fillcolor
Default fillcolor for time vis events.
hover_label_settings
Plotly hover label settings.
rh
Height of each rectangular time vis.
y_space
Vertical spacing between rectangles.
Instance variables⚓︎
axis_range
Specify x/y axis range or leave as empty dictionary for autorange.
Methods⚓︎
apply_custom_layout⚓︎
def apply_custom_layout(
self,
layout
)
Extend and/or override layout with custom settings.
Parameters:
| Name | Description |
|---|---|
| layout | base layout dictionary. Typically from self.create_layout() |
Returns:
| Type | Description |
|---|---|
| dict | layout for Dash figure |
View Source
def apply_custom_layout(self, layout):
"""Extend and/or override layout with custom settings.
Args:
layout: base layout dictionary. Typically from self.create_layout()
Returns:
dict: layout for Dash figure
"""
for parent_key, sub_key, value in self.layout_overrides:
if sub_key is not None:
layout[parent_key][sub_key] = value
else:
layout[parent_key] = value
return layout
create_figure⚓︎
def create_figure(
self,
df_raw,
**kwargs_data
)
Create the figure dictionary.
Parameters:
| Name | Description |
|---|---|
| df_raw | data to pass to formatter method |
| kwargs_data | keyword arguments to pass to the data formatter method |
Returns:
| Type | Description |
|---|---|
| dict | keys data and layout for Dash |
View Source
def create_figure(self, df_raw, **kwargs_data):
"""Create the figure dictionary.
Args:
df_raw: data to pass to formatter method
kwargs_data: keyword arguments to pass to the data formatter method
Returns:
dict: keys `data` and `layout` for Dash
"""
return {
'data': self.create_traces(df_raw, **kwargs_data),
'layout': go.Layout(self.apply_custom_layout(self.create_layout())),
}
create_layout⚓︎
def create_layout(
self
)
Extend the standard layout.
Returns:
| Type | Description |
|---|---|
| dict | layout for Dash figure |
View Source
def create_layout(self):
"""Extend the standard layout.
Returns:
dict: layout for Dash figure
"""
layout = super().create_layout()
# Set YAxis tick marks for category names (https://plotly.com/python/tick-formatting)
layout['yaxis']['tickmode'] = 'array'
layout['yaxis']['tickvals'] = np.subtract(
np.multiply(
np.array(range(len(self.categories))),
self.y_space,
),
self.rh / 2,
)
layout['yaxis']['ticktext'] = [*self.categories]
layout['yaxis']['zeroline'] = False
# Hide legend
layout['legend'] = {}
layout['showlegend'] = False
# Add shapes and append new annotations
layout['shapes'] = self._shapes
layout['annotations'] += self._annotations
return layout
create_traces⚓︎
def create_traces(
self,
df_raw
)
Return traces for plotly chart.
Parameters:
| Name | Description |
|---|---|
| df_raw | pandas dataframe with columns: (category, label, start, end) |
Returns:
| Type | Description |
|---|---|
| list | Dash chart traces |
View Source
def create_traces(self, df_raw): # noqa: CCR001
"""Return traces for plotly chart.
Args:
df_raw: pandas dataframe with columns: `(category, label, start, end)`
Returns:
list: Dash chart traces
"""
# Get all unique category names and create lookup for y positions
self.categories = sorted(cat for cat in set(df_raw['category'].tolist()) if cat)
y_pos_lookup = {cat: self.y_space * idx for idx, cat in enumerate(self.categories)}
# Create the Time Vis traces
traces = []
self._shapes = []
self._annotations = []
for vis in df_raw.itertuples():
if vis.category in y_pos_lookup:
y_pos = y_pos_lookup[vis.category]
if vis.end:
traces.append(self._create_time_vis_shape(vis, y_pos))
if vis.label:
traces.append(self._create_annotation(vis, y_pos))
else:
traces.append(self._create_event(vis, y_pos))
else:
y_pos = 0
traces.append(self._create_non_cat_shape(vis, y_pos))
return traces
initialize_mutables⚓︎
def initialize_mutables(
self
)
Initialize the mutable data members to prevent modifying one attribute and impacting all instances.
View Source
def initialize_mutables(self):
"""Initialize the mutable data members to prevent modifying one attribute and impacting all instances."""
...
Last update:
August 5, 2022
Created: August 5, 2022
Created: August 5, 2022